import json
import logging
import os
import re
import time
from dataclasses import dataclass, field
from typing import Dict, Iterable, List, Optional, Sequence, Tuple

from dotenv import load_dotenv

try:  # The OpenAI SDK is optional during local development.
    from openai import OpenAI
except Exception:  # pragma: no cover - handled at runtime.
    OpenAI = None  # type: ignore

from .board import Board
from .player import Player


load_dotenv()

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

if not logger.handlers:
    handler = logging.StreamHandler()
    formatter = logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    handler.setFormatter(formatter)
    logger.addHandler(handler)


@dataclass
class ScoredMove:
    move: Tuple[int, int]
    reason: str = ""
    llm_rank: int = -1
    attack_score: float = 0.0
    defense_score: float = 0.0
    total_score: float = 0.0
    unstoppable: bool = False
    tags: List[str] = field(default_factory=list)
    confidence: float = 0.0


@dataclass
class LLMAnalysisResult:
    best_move: Optional[Tuple[int, int]] = None
    candidate_moves: List[Tuple[int, int]] = field(default_factory=list)
    scored_moves: List[ScoredMove] = field(default_factory=list)
    analysis: str = ""
    raw_response: str = ""


@dataclass
class TacticalScore:
    move: Tuple[int, int]
    attack: float
    defense: float
    unstoppable: bool
    tags: List[str] = field(default_factory=list)


class TacticalEngine:
    """Non-LLM tactical core that searches for forced wins and evaluates moves."""

    _directions: Sequence[Tuple[int, int]] = (
        (0, 1),
        (1, 0),
        (1, 1),
        (1, -1),
    )

    def __init__(self, search_radius: int = 3) -> None:
        self.search_radius = search_radius

    def find_forced_move(
        self,
        board: Board,
        player: Player,
        opponent: Player,
    ) -> Optional[Tuple[int, int]]:
        winning_moves = self.immediate_wins(board, player)
        if winning_moves:
            logger.debug("TacticalEngine: found immediate win %s", winning_moves[0])
            return winning_moves[0]

        opponent_wins = self.immediate_wins(board, opponent)
        if opponent_wins:
            logger.debug("TacticalEngine: blocking opponent win %s", opponent_wins[0])
            return opponent_wins[0]

        double_threats = self.double_threat_moves(board, player)
        if double_threats:
            logger.debug("TacticalEngine: creating double threat %s", double_threats[0])
            return double_threats[0]

        fours = self.threatening_fours(board, player)
        if fours:
            logger.debug("TacticalEngine: building four threat %s", fours[0])
            return fours[0]

        opponent_double_threats = self.double_threat_moves(board, opponent)
        for move in opponent_double_threats:
            if board.is_valid_move(*move):
                logger.debug(
                    "TacticalEngine: denying opponent double threat %s", move
                )
                return move

        opponent_fours = self.threatening_fours(board, opponent)
        for move in opponent_fours:
            if board.is_valid_move(*move):
                logger.debug("TacticalEngine: blocking opponent four %s", move)
                return move

        return None

    def best_static_move(
        self,
        board: Board,
        player: Player,
        opponent: Player,
    ) -> Optional[Tuple[int, int]]:
        candidates = self.candidate_moves(board)
        scored = self.evaluate_moves(board, candidates, player, opponent)
        if not scored:
            return None

        scored.sort(key=lambda s: (s.unstoppable, s.attack + s.defense), reverse=True)
        return scored[0].move

    def describe_position(
        self,
        board: Board,
        player: Player,
        opponent: Player,
        coord_to_notation,
        limit: int = 6,
    ) -> str:
        wins_player = [coord_to_notation(*m) for m in self.immediate_wins(board, player)]
        wins_opponent = [
            coord_to_notation(*m) for m in self.immediate_wins(board, opponent)
        ]
        doubles_player = [
            coord_to_notation(*m) for m in self.double_threat_moves(board, player)
        ]
        doubles_opponent = [
            coord_to_notation(*m) for m in self.double_threat_moves(board, opponent)
        ]
        fours_player = [
            coord_to_notation(*m) for m in self.threatening_fours(board, player)
        ]
        fours_opponent = [
            coord_to_notation(*m) for m in self.threatening_fours(board, opponent)
        ]

        summary_lines: List[str] = []
        if wins_player:
            summary_lines.append(f"白方一手胜势: {', '.join(wins_player)}")
        if wins_opponent:
            summary_lines.append(f"黑方一手胜势: {', '.join(wins_opponent)}")
        if doubles_player:
            summary_lines.append(f"白方双重威胁: {', '.join(doubles_player)}")
        if doubles_opponent:
            summary_lines.append(f"黑方双重威胁: {', '.join(doubles_opponent)}")
        if fours_player:
            summary_lines.append(f"白方冲四: {', '.join(fours_player)}")
        if fours_opponent:
            summary_lines.append(f"黑方冲四: {', '.join(fours_opponent)}")

        scores = self.evaluate_moves(
            board,
            self.candidate_moves(board),
            player,
            opponent,
        )
        scores.sort(key=lambda s: (s.unstoppable, s.attack + s.defense), reverse=True)

        if scores:
            summary_lines.append("静态评估前几名：")
            for idx, score in enumerate(scores[:limit], start=1):
                move_text = coord_to_notation(*score.move)
                tags = " ".join(score.tags)
                summary_lines.append(
                    f"{idx}. {move_text} 攻{score.attack:.0f} 防{score.defense:.0f} {tags}".rstrip()
                )

        return "\n".join(summary_lines)

    # --- core tactical helpers -------------------------------------------------

    def candidate_moves(
        self,
        board: Board,
        radius: Optional[int] = None,
    ) -> List[Tuple[int, int]]:
        radius = radius if radius is not None else self.search_radius
        stones: List[Tuple[int, int]] = [
            (r, c)
            for r in range(board.size)
            for c in range(board.size)
            if board.board[r][c] != 0
        ]

        if not stones:
            center = board.size // 2
            return [(center, center)]

        candidates = set()
        for row, col in stones:
            for dr in range(-radius, radius + 1):
                for dc in range(-radius, radius + 1):
                    if dr == 0 and dc == 0:
                        continue
                    nr, nc = row + dr, col + dc
                    if 0 <= nr < board.size and 0 <= nc < board.size:
                        if board.board[nr][nc] == 0:
                            candidates.add((nr, nc))

        if not candidates:
            center = board.size // 2
            return [(center, center)]
        return list(candidates)

    def immediate_wins(self, board: Board, player: Player) -> List[Tuple[int, int]]:
        result: List[Tuple[int, int]] = []
        for move in self.candidate_moves(board, radius=2):
            row, col = move
            if not board.is_valid_move(row, col):
                continue
            board.place_stone(row, col, player.value)
            try:
                if board.check_winner(row, col):
                    result.append(move)
            finally:
                board.undo()
        return result

    def double_threat_moves(
        self,
        board: Board,
        player: Player,
    ) -> List[Tuple[int, int]]:
        unstoppable_moves: List[Tuple[int, int]] = []
        for move in self.candidate_moves(board, radius=3):
            row, col = move
            if not board.is_valid_move(row, col):
                continue

            board.place_stone(row, col, player.value)
            try:
                if board.check_winner(row, col):
                    unstoppable_moves.append(move)
                    continue

                followups = self.immediate_wins(board, player)
                if len(followups) >= 2:
                    unstoppable_moves.append(move)
            finally:
                board.undo()

        return unstoppable_moves

    def threatening_fours(
        self,
        board: Board,
        player: Player,
    ) -> List[Tuple[int, int]]:
        threats: List[Tuple[int, int]] = []
        for move in self.candidate_moves(board, radius=3):
            row, col = move
            if not board.is_valid_move(row, col):
                continue
            board.place_stone(row, col, player.value)
            try:
                if self._forms_four(board, row, col, player.value):
                    threats.append(move)
            finally:
                board.undo()
        return threats

    def evaluate_moves(
        self,
        board: Board,
        moves: Iterable[Tuple[int, int]],
        player: Player,
        opponent: Player,
    ) -> List[TacticalScore]:
        opponent_wins_before = set(self.immediate_wins(board, opponent))
        opponent_double_threats = set(self.double_threat_moves(board, opponent))
        opponent_four_threats = set(self.threatening_fours(board, opponent))

        scores: List[TacticalScore] = []
        for row, col in moves:
            if not board.is_valid_move(row, col):
                continue
            scores.append(
                self._evaluate_single_move(
                    board,
                    (row, col),
                    player,
                    opponent,
                    opponent_wins_before,
                    opponent_double_threats,
                    opponent_four_threats,
                )
            )
        return scores

    def _evaluate_single_move(
        self,
        board: Board,
        move: Tuple[int, int],
        player: Player,
        opponent: Player,
        opponent_wins_before: Iterable[Tuple[int, int]],
        opponent_double_threats: Iterable[Tuple[int, int]],
        opponent_four_threats: Iterable[Tuple[int, int]],
    ) -> TacticalScore:
        row, col = move
        board.place_stone(row, col, player.value)
        tags: List[str] = []
        unstoppable = False

        try:
            if board.check_winner(row, col):
                attack = 1_000_000
                unstoppable = True
                tags.append("instant-win")
            else:
                attack = self._attack_score(board, row, col, player)
                if self._forms_four(board, row, col, player.value):
                    attack += 120_000
                    if "double-threat" not in tags:
                        tags.append("make-four")

            followups = self.immediate_wins(board, player)
            if len(followups) >= 2:
                unstoppable = True
                attack += 300_000
                tags.append("double-threat")
            elif len(followups) == 1:
                attack += 60_000

            defense = 0.0
            if move in opponent_wins_before:
                defense += 120_000
                tags.append("block")
            if move in opponent_double_threats:
                defense += 90_000
                if "block" not in tags:
                    tags.append("deny-double")
            if move in opponent_four_threats:
                defense += 80_000
                if "block" not in tags:
                    tags.append("block-four")

            opponent_wins_after = self.immediate_wins(board, opponent)
            if not opponent_wins_after:
                defense += 6_000
            else:
                defense -= 150_000 * len(opponent_wins_after)

            opponent_double_after = self.double_threat_moves(board, opponent)
            if opponent_double_after:
                defense -= 200_000 * len(opponent_double_after)

            return TacticalScore(
                move=move,
                attack=attack,
                defense=defense,
                unstoppable=unstoppable,
                tags=tags,
            )
        finally:
            board.undo()

    def _attack_score(self, board: Board, row: int, col: int, player: Player) -> float:
        score = 0.0
        player_value = player.value
        for dr, dc in self._directions:
            count, open_ends = self._count_line(board, row, col, dr, dc, player_value)
            score += self._pattern_score(count, open_ends)
        return score

    def _forms_four(self, board: Board, row: int, col: int, player_value: int) -> bool:
        for dr, dc in self._directions:
            count, open_ends = self._count_line(board, row, col, dr, dc, player_value)
            if count >= 4 and open_ends >= 1:
                return True
        return False

    def _count_line(
        self,
        board: Board,
        row: int,
        col: int,
        dr: int,
        dc: int,
        player_value: int,
    ) -> Tuple[int, int]:
        count = 1
        open_ends = 0

        r, c = row + dr, col + dc
        while 0 <= r < board.size and 0 <= c < board.size and board.board[r][c] == player_value:
            count += 1
            r += dr
            c += dc
        if 0 <= r < board.size and 0 <= c < board.size and board.board[r][c] == 0:
            open_ends += 1

        r, c = row - dr, col - dc
        while 0 <= r < board.size and 0 <= c < board.size and board.board[r][c] == player_value:
            count += 1
            r -= dr
            c -= dc
        if 0 <= r < board.size and 0 <= c < board.size and board.board[r][c] == 0:
            open_ends += 1

        return count, open_ends

    def _pattern_score(self, count: int, open_ends: int) -> float:
        if count >= 5:
            return 800_000
        if count == 4:
            return 200_000 if open_ends == 2 else 120_000
        if count == 3:
            return 50_000 if open_ends == 2 else 18_000
        if count == 2:
            return 6_000 if open_ends == 2 else 2_000
        if count == 1:
            return 600 if open_ends == 2 else 200
        return 50.0


class GomokuLLMAgent:
    """LLM-backed agent with tactical guarantees that humans struggle to beat."""

    def __init__(self) -> None:
        if OpenAI is None:
            raise RuntimeError("未安装 openai SDK，无法启用大模型对弈功能")

        logger.info("初始化 GomokuLLMAgent（强化版）")

        api_key = os.getenv("OPENAI_API_KEY")
        if not api_key:
            raise RuntimeError("OPENAI_API_KEY 未设置，无法启用大模型对弈功能")

        self.model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
        self.temperature = float(os.getenv("OPENAI_TEMPERATURE", "0.05"))
        self.max_retries = int(os.getenv("OPENAI_RETRY", "3"))
        self.fast_move_delay = float(os.getenv("OPENAI_FAST_MOVE_DELAY", "2.0"))
        self.cache: Dict[str, LLMAnalysisResult] = {}
        self.tactics = TacticalEngine()

        base_url = os.getenv("OPENAI_BASE_URL")
        organization = os.getenv("OPENAI_ORGANIZATION")

        client_kwargs = {"api_key": api_key}
        if base_url:
            client_kwargs["base_url"] = base_url
        if organization:
            client_kwargs["organization"] = organization

        try:
            self.client = OpenAI(**client_kwargs)
        except Exception as exc:  # pragma: no cover - runtime configuration
            logger.error("OpenAI 客户端初始化失败: %s", exc)
            raise

        logger.info(
            "GomokuLLMAgent 配置: model=%s temperature=%.2f retries=%d",
            self.model,
            self.temperature,
            self.max_retries,
        )

    # --------------------------------------------------------------------- public

    def analyze(self, board: Board) -> LLMAnalysisResult:
        signature = self._board_signature(board)
        cached = self.cache.get(signature)
        if cached:
            logger.debug("命中局面缓存，跳过重新调用大模型")
            return cached

        prompt = self._build_prompt(board)
        messages = [
            {
                "role": "system",
                "content": (
                    "你是五子棋世界冠军，目标是在任何情况下确保白棋不可被击败。"
                    "优先考虑立即胜利、不可同时封堵的双重威胁，以及所有强制先手。"
                    "请严格输出JSON，不要携带多余文字。"
                ),
            },
            {"role": "user", "content": prompt},
        ]

        result = LLMAnalysisResult()
        attempts = 0
        while attempts <= self.max_retries:
            attempts += 1
            try:
                response = self.client.chat.completions.create(
                    model=self.model,
                    temperature=self.temperature,
                    messages=messages,
                )
            except Exception as exc:
                logger.warning("调用大模型失败（第 %d 次尝试）: %s", attempts, exc)
                if attempts > self.max_retries:
                    break
                continue

            content = response.choices[0].message.content if response.choices else ""
            if not content:
                logger.warning("大模型返回空内容（第 %d 次尝试）", attempts)
                if attempts > self.max_retries:
                    break
                continue

            result = self._extract_analysis(content, board)
            if result.candidate_moves:
                self.cache[signature] = result
                return result

            messages.append({"role": "assistant", "content": content})
            messages.append(
                {
                    "role": "user",
                    "content": (
                        "回答缺少有效走法，请返回如下格式："
                        '{"analysis":"...","candidate_moves":[{"move":"H8","reason":"..."},'
                        '{"move":"J9","reason":"..."}],"best_move":"H8"}'
                    ),
                }
            )

        logger.warning("大模型分析失败，返回空结果")
        return result

    def suggest_move(self, board: Board) -> Optional[Tuple[int, int]]:
        player = Player.WHITE
        opponent = Player.BLACK

        forced = self.tactics.find_forced_move(board, player, opponent)
        if forced:
            self._delay_fast_move("forced")
            logger.info(
                "tactics 优先走法: %s", self._coord_to_notation(*forced)
            )
            return forced

        result = self.analyze(board)
        if result.scored_moves:
            for scored in result.scored_moves:
                if board.is_valid_move(*scored.move):
                    logger.info(
                        "采用大模型候选: %s 标签=%s 分数=%.0f",
                        self._coord_to_notation(*scored.move),
                        ",".join(scored.tags),
                        scored.total_score,
                    )
                    return scored.move

        for move in result.candidate_moves:
            if board.is_valid_move(*move):
                logger.info(
                    "回退到未评分候选: %s", self._coord_to_notation(*move)
                )
                return move

        fallback = self.tactics.best_static_move(board, player, opponent)
        if fallback and board.is_valid_move(*fallback):
            self._delay_fast_move("fallback")
            logger.info(
                "使用静态评估回退: %s", self._coord_to_notation(*fallback)
            )
            return fallback

        logger.warning("未能找到可落子的有效点")
        return None

    # ------------------------------------------------------------------ builders

    def _build_prompt(self, board: Board) -> str:
        board_text = self._format_board(board)
        last_move = self._coord_to_notation(*board.last_move) if board.last_move else "无"
        tactical_snapshot = self.tactics.describe_position(
            board,
            Player.WHITE,
            Player.BLACK,
            self._coord_to_notation,
        )

        return (
            "棋盘说明：15x15，列 A-O，行 1-15。\n"
            f"上一手：{last_move}\n"
            f"{board_text}\n\n"
            "当前局面评估（供参考）：\n"
            f"{tactical_snapshot or '暂无直接威胁总结'}\n\n"
            "请完成以下任务：\n"
            "1. 识别黑白双方的必胜威胁与关键弱点。\n"
            "2. 列出至少三个白方候选走法，逐一说明如何保持先手或者形成不可同时封堵的威胁。\n"
            "3. 选出最强走法，保证即使对手完美防守也无法击败白方。\n"
            "输出格式（仅允许 JSON ）：\n"
            '{\n'
            '  "analysis": "整体思路",\n'
            '  "candidate_moves": [\n'
            '    {"move": "H8", "reason": "形成双活三", "confidence": 0.92, "tags": ["double-threat"]},\n'
            '    {"move": "J9", "reason": "阻断黑方活四"}\n'
            '  ],\n'
            '  "best_move": "H8"\n'
            '}\n'
            "务必只返回 JSON，禁止含有额外文本。"
        )

    def _format_board(self, board: Board) -> str:
        header = "   " + " ".join(chr(ord("A") + i) for i in range(board.size))
        lines = [header]
        for idx, row in enumerate(board.board):
            display = []
            for cell in row:
                if cell == Player.NONE.value:
                    display.append(".")
                elif cell == Player.BLACK.value:
                    display.append("B")
                else:
                    display.append("W")
            lines.append(f"{idx + 1:2} {' '.join(display)}")
        return "\n".join(lines)

    def _coord_to_notation(self, row: int, col: int) -> str:
        return f"{chr(ord('A') + col)}{row + 1}"

    # ------------------------------------------------------------------ parsing

    def _extract_analysis(self, content: str, board: Board) -> LLMAnalysisResult:
        result = LLMAnalysisResult(raw_response=content or "")
        if not content:
            return result

        json_text = self._locate_json(content)
        if not json_text:
            logger.warning("未能在响应中找到 JSON 结构")
            return result

        try:
            data = json.loads(json_text)
        except json.JSONDecodeError as exc:
            logger.warning("JSON 解析失败: %s", exc)
            return result

        result.analysis = (
            data.get("analysis", "") if isinstance(data.get("analysis"), str) else ""
        )

        best_move_str = data.get("best_move")
        if isinstance(best_move_str, str):
            best_tuple = self._parse_move(best_move_str, board.size)
            if best_tuple:
                result.best_move = best_tuple

        candidates_data = data.get("candidate_moves", [])
        if isinstance(candidates_data, list):
            for idx, item in enumerate(candidates_data):
                if isinstance(item, dict):
                    move_str = item.get("move")
                    if not isinstance(move_str, str):
                        continue
                    move = self._parse_move(move_str, board.size)
                    if not move:
                        continue
                    reason = (
                        item.get("reason")
                        or item.get("plan")
                        or item.get("analysis")
                        or ""
                    )
                    if not isinstance(reason, str):
                        reason = ""
                    confidence = item.get("confidence")
                    if isinstance(confidence, (int, float)):
                        conf_value = float(confidence)
                    else:
                        conf_value = 0.0
                    tags = item.get("tags", [])
                    if isinstance(tags, list):
                        tags_list = [str(tag) for tag in tags]
                    else:
                        tags_list = []
                    scored = ScoredMove(
                        move=move,
                        reason=reason,
                        llm_rank=idx,
                        confidence=conf_value,
                        tags=tags_list,
                    )
                    result.scored_moves.append(scored)
                elif isinstance(item, str):
                    move = self._parse_move(item, board.size)
                    if move:
                        scored = ScoredMove(move=move, llm_rank=idx)
                        result.scored_moves.append(scored)

        if result.best_move and all(
            scored.move != result.best_move for scored in result.scored_moves
        ):
            result.scored_moves.insert(
                0,
                ScoredMove(move=result.best_move, llm_rank=0, reason="best_move 字段"),
            )

        if not result.scored_moves and result.best_move:
            result.scored_moves.append(
                ScoredMove(move=result.best_move, llm_rank=0, reason="fallback best")
            )

        self._score_candidate_moves(board, result)

        result.candidate_moves = [scored.move for scored in result.scored_moves]
        if result.candidate_moves and (
            not result.best_move or result.best_move not in result.candidate_moves
        ):
            result.best_move = result.candidate_moves[0]

        return result

    def _score_candidate_moves(self, board: Board, result: LLMAnalysisResult) -> None:
        if not result.scored_moves:
            return

        player = Player.WHITE
        opponent = Player.BLACK

        tactical_scores = self.tactics.evaluate_moves(
            board,
            [scored.move for scored in result.scored_moves],
            player,
            opponent,
        )
        score_map: Dict[Tuple[int, int], TacticalScore] = {
            score.move: score for score in tactical_scores
        }

        for scored in result.scored_moves:
            tactical = score_map.get(scored.move)
            if not tactical:
                continue
            scored.attack_score = tactical.attack
            scored.defense_score = tactical.defense
            scored.unstoppable = scored.unstoppable or tactical.unstoppable
            for tag in tactical.tags:
                if tag not in scored.tags:
                    scored.tags.append(tag)
            unstoppable_bonus = 500_000 if scored.unstoppable else 0.0
            scored.total_score = (
                scored.attack_score + scored.defense_score + unstoppable_bonus
            )

        result.scored_moves.sort(
            key=lambda m: (
                m.unstoppable,
                m.total_score,
                m.confidence,
                -m.llm_rank,
            ),
            reverse=True,
        )

    def _locate_json(self, content: str) -> Optional[str]:
        content = content.strip()
        if content.startswith("{") and content.endswith("}"):
            return content
        start = content.find("{")
        end = content.rfind("}")
        if start != -1 and end != -1 and end > start:
            return content[start : end + 1]
        return None

    def _parse_move(self, text: str, size: int) -> Optional[Tuple[int, int]]:
        if not text:
            return None
        pattern = re.compile(r"([A-Oa-o])\s*(1[0-5]|[1-9])")
        match = pattern.search(text)
        if not match:
            return None
        col_letter = match.group(1).upper()
        row_number = int(match.group(2))
        col = ord(col_letter) - ord("A")
        row = row_number - 1
        if 0 <= row < size and 0 <= col < size:
            return row, col
        return None

    def _board_signature(self, board: Board) -> str:
        rows = ["".join(str(cell) for cell in row) for row in board.board]
        return "|".join(rows)

    def _delay_fast_move(self, reason: str) -> None:
        delay = max(0.0, self.fast_move_delay)
        if delay <= 0.0:
            return
        logger.debug("执行快速落子前延迟 %.2fs（%s）", delay, reason)
        time.sleep(delay)
